# -*- coding: utf-8 -*-
"""
:mod:`nubox.core`
=================

Provides the ``NdArray`` class, a N-dimensional array the more fancy ``NuBox`` 
and the  a basic hierarchy of sub-classes, which include ``NuBoxScalar``, 
``NuBoxVector``, ``NuBoxArray`` and ``NuBoxCube`` which all are sub-classes of 
``NdArray`` to represent hierarchical data and metadata.

"""
from __future__ import (division, with_statement, absolute_import, generators,
                         nested_scopes)
import cPickle as pickle
import operator
import zlib
from copy import deepcopy, copy
from array import array
from math import floor

from .shim import (unicode, long, int, reduce, izip, xrange)
from .const import (ANY, DEFAULT, NOCHILD)
from .util import checkitem, checktypecode, gettype, slices_shape2indices_shape
from nubox.util import compatypecodes


class NdArray(object):
    """
    An N-dimensional **array** of N-dimensional **items**. An **array** is 
    constructed from a iterable "data" container, a "shape" and a "typecode". 
    The "typecode" determines the type of data held in the **array** e.g. "l" 
    for ``long`` or "o" for arbitrary ``object`` instances. The "shape" 
    determines the number and lengths of the dimensions. Sub-arrays along the 
    first dimension are called **children**. Sub-arrays along the next to last 
    dimension are called **items**. The length of the last dimension of the 
    **array** determines the dimension of the **item**. Arbitrary slices of 
    the array are called **chunks**. Objects in the flattened **array** are 
    called **elements**.
    
    Sub-array naming convention:
    
      - **array** refers to the whole ``NdArray`` instance 
      - **chunk** is and arbitrary multi-dimensional slice of an **array**
      - **child** is a sub-array indexed along the *first* dimension 
      - **item** is a sub-array indexed along the *next to last* dimension 
      - **element** is an object of the flattened **array** 
      
    ``NdArray`` provides methods and syntactic sugar to access all the sub-array
    types conveniently:
    
      - **chunk** *via* ``array.get_chunk``, ``array.set_chunk`` or 
        ``array[<slice>]`` returns: ``NdArray``
      - **child** *via* ``array.get_child``, ``array.set_child`` or 
        ``array[<int>]`` returns: ``NdArray``
      - **item** *via* ``array.get_item`` or ``array.set_item`` returns:
        ``tuple``
      - **element** *via* ``array[<tuple>]`` returns: ``type``
    
    An ``NdArray`` has the following proprties, which are also mandatory 
    constructor arguments.:
    
      - "data" contains the raw data converted to container defined by "store"
      - "shape" defines the number and length of **dimensions** of the array
      - "typecode" defines the ``type`` of **elements** stored in "data"
      - "store" defines the ``type`` of the data container
    
    These properties are constructed from the following arguments:
    
      - data (iterable or ``None``) any iterable of **elements**
      - shape (iterable) a tuple of ``int``.
      - typecode (``str``) a single character, must be a valid letter for 
        ``array.array`` or ``"o"`` if "store" is ``"list"``.
        
    Additional arguments provide defaults:
    
      - store (``"array"`` or ``"list"``) [default: ``"array"``] ``"array"`` 
        corresponds to ``array.array`` and ``"list"`` to ``list``.
      - copy (``bool``) [default: ``True``] If ``False`` the "data" will not be
        copied if it is in the same "store" ``type``. Will raise a 
        ``ValueError`` if ``False``, but a copy is required.
            
    """
    def __init__(self, data, shape, typecode, store="array", copy=True):

        self._setshape(shape)  # uses property, sets shape and short
        self._setdata((store, typecode, data, copy))

    def __eq__(self, other):
        return self.data == getattr(other, 'data', None) and \
               self.meta == getattr(other, 'meta', None)

    def __str__(self):
        name = self.__class__.__name__
        if isinstance(self.data, array):
            return self.data.__str__().replace('array', name)
        else:
            return "".join((name , '(', self.data.__str__(), ')'))

#
# properties
#

    def _gettypecode(self):
        return self.__typecode

    def _settypecode(self, typecode):
        raise AttributeError("protected property")

    def _getstore(self):
        return self.__store

    def _setstore(self, store):
        raise AttributeError("protected property")

    def _getshort(self):
        return self.__short

    def _setshort(self, short):
        raise AttributeError("protected property")

    def _getshape(self):
        return self.__shape

    def _setshape(self, shape):
        try:
            shape = tuple(shape)
        except TypeError:
            raise TypeError("shape is not iterable")
        short = []
        shrt = 1
        for i in range(len(shape) - 1, -1, -1):
            short.insert(0, shrt)
            shrt *= shape[i]
        self.__short = tuple(short)
        self.__shape = shape

    def _getdata(self):
        return self.__data

    def _setdata(self, store_typecode_data_copy):
        store, typecode, data, copy_ = store_typecode_data_copy
        self._checkdata(data, typecode, store)
        # shape and size
        if self.shape and len(data) != reduce(operator.mul, self.shape):
            raise ValueError("total size of data must match shape")
        if store == 'array':
            try:
                if (not copy_):
                    if not isinstance(data, array) or typecode != data.typecode:
                        raise ValueError("copy required")
                    self.__data = data
                elif isinstance(data, array) and typecode == data.typecode:
                    self.__data = copy(data)
                else:
                    self.__data = array(typecode, data)
            except TypeError:
                raise TypeError("data must match typecode")
        elif store == 'list':
            if (not copy_):
                if not isinstance(data, list):
                    raise ValueError("copy required")
                self.__data = data
            else:
                self.__data = list(data)
        else:
            raise ValueError("unknown store type")
        self.__typecode = typecode
        self.__store = store

    short = property(_getshort, _setshort)
    store = property(_getstore, _setstore)
    typecode = property(_gettypecode, _settypecode)
    shape = property(_getshape, _setshape)
    data = property(_getdata, _setdata)

#
# nD logic
#

    def _checkdata(self, data, typecode, store):
        istc, isatc, isutc, isotc, isftc, isitc = checktypecode(typecode) #@UnusedVariable
        # iterable input
        try:

            iter(data)
        except TypeError:
            raise TypeError("data is not iterable")
        # list and array
        if not istc:
            raise ValueError("bad typecode")
        # only list
        if not isatc and store == 'array':
            raise ValueError("bad typecode for array")
        if data:
            item = data[0]
            if isutc and not isinstance(item, unicode):
                raise TypeError("data must match unicode typecode")
            if isftc and not isinstance(item, float):
                raise TypeError("data must match double typecode")
            if isitc and not isinstance(item, (int, long)):
                raise TypeError("data must match integer-like typecode")

    def _flattenindex(self, index):
        try:
            index = tuple(index)
        except TypeError:
            raise TypeError("array indices must be iterable, got %s" % \
                            type(index).__name__)
        if len(index) != len(self.shape):
            raise IndexError("invalid index")

        offset = 0
        for idx, srt, dim in zip(index, self.short, self.shape):
            if not isinstance(idx, int):
                raise TypeError("array index must be int, got %s" % \
                                type(idx).__name__)
            if not 0 <= idx < dim:
                raise IndexError("array index out of range")
            offset += srt * idx
        return offset

    def _itemdata(self, indices):
        itembounds = self._itembounds()
        data = self.data[0:0]
        for index in indices:
            start, stop = itembounds[index]
            data.extend(self.data[start:stop])
        return tuple(data)

    def _itembounds(self):
        step = self.shape[-1]
        stop = len(self.data)
        starts = xrange(0, stop, step)
        stops = xrange(step, stop + step, step)
        return zip(starts, stops)

    def _childrenstep(self, data=None):
        child_num = self.shape[0]
        data = data or self.data
        step = int(len(data) / child_num)
        return step

    def _childrenbounds(self, data=None, step=None):
        data = data or self.data
        step = step or self._childrenstep(data)
        return tuple([(st, st + step) for st in range(0, len(data), step)])

    def _childrendata(self, indices):
        childbounds = self._childrenbounds()
        data = self.data[0:0] # preserve store
        for index in indices:
            start, stop = childbounds[index]
            data.extend(self.data[start:stop])
        return data

    def _chunkdata(self, indices):
        data = self.data[0:0]
        for index in indices:
            index = self._flattenindex(index)
            data.append(self.data[index])
        return data

#
# overloading & syntax sugar
#

    def __getitem__(self, index):
        if isinstance(index, slice):
            indices = xrange(*index.indices(len(self))) # index is of slice type
            data = self._childrendata(indices)
            shape = (len(indices),) + self.shape[1:]
            return self.new(data, shape=shape, copy=False)
        elif isinstance(index, tuple):
            index = self._flattenindex(index)
            return self.data[index]
        elif isinstance(index, int):
            start, stop = self._childrenbounds()[index]
            shape = self.shape[1:]
            return self.new(self.data[start:stop], shape=shape, copy=False)
        else:
            raise TypeError("index must be tuple or int")

    def __setitem__(self, index, value):
        if isinstance(index, slice):
            indices = xrange(*index.indices(len(self)))
            # determine the step based on the data
            step = self._childrenstep()
            # determine the childbounds based on the data and the step
            childbounds = self._childrenbounds(step=step)
            # determine the valuebounds based on the
            valuebounds = self._childrenbounds(data=value, step=step)
            for value_index, child_index in enumerate(indices):
                l_start, l_stop = childbounds[child_index]
                r_start, r_stop = valuebounds[value_index]
                try:
                    self.data[l_start:l_stop] = value[r_start:r_stop]
                except TypeError:
                    # non-array to array assignment
                    for j, k in izip(xrange(l_start, l_stop), \
                                     xrange(r_start, r_stop)):
                        self.data[j] = value[k]

        elif isinstance(index, tuple):
            index = self._flattenindex(index)
            self.data[index] = value

        elif isinstance(index, int):
            start, stop = self._childrenbounds()[index]
            try:
                self.data[start:stop] = value
            except TypeError:
                # non-array to array assignment
                for i, j in enumerate(xrange(start, stop)):
                    self.data[j] = value[i]
        else:
            raise TypeError("index must be tuple or int")

    def __iter__(self):
        childbounds = self._childrenbounds()
        shape = self.shape[1:]
        for start, stop in childbounds:
            yield self.new(self.data[start:stop], shape=shape, copy=False)

    def _iterfast(self):
        dummy = self.get_child(0)
        childbounds = self._childrenbounds()
        for start, stop in childbounds:
            dummy.__data = self.data[start:stop]
            yield dummy

    def __len__(self):
        return self.shape[0]

# persistance


    def _kwargs(self):
        kwargs = {
         'shape':self.shape,
         'typecode':self.typecode,
         'store':self.store}
        return kwargs

    def __copy__(self):
        # a shallow copy
        kwargs = self._kwargs()
        kwargs['copy'] = True
        return self.__class__(self.data, **kwargs)

    def __deepcopy__(self, memo):
        return self.new()

    def new(self, data=None, **kwargs):
        """
        Creates a new instance of ``NdArray`` has a signature identical to the 
        default constructor with the key difference that it by default clones
        all properties of the current instance. If no arguments are given this 
        is identical to ``deepcopy(self)``.
        
        see: ``NdArray.__init__``
        
        Keyworded arguments are the same as for the ``NuBox`` constructor.
          
        """
        new_kwargs = self._kwargs()
        new_kwargs['copy'] = False # why copy twice?
        new_kwargs.update(kwargs)
        data = data or deepcopy(self.data)
        new = self.__class__(data, **new_kwargs)
        return new

    def dump(self, filepath=None):
        """
        Dumps a ``NdArray`` into a package. 
        
        Arguments:
        
          - filepath(``str``) a file path with ".nubox" extension (will be 
            erased if exists)
        
        """
        data = pickle.dumps(self, pickle.HIGHEST_PROTOCOL)
        pack = zlib.compress(data)
        if filepath:
            if not filepath.endswith(".nubox"):
                raise ValueError("Expected file name with nubox extension.")
            fh = open(filepath, 'wb')
            fh.write(pack)
            fh.close()
        else:
            filepath = pack
        return filepath

    def load(self, filepath=None):
        """
        Loads an ``NdArray`` from a package.

          - filepath(``str``) a file path with ".nubox" extension
        
        """
        if filepath.endswith(".nubox"):
            fh = open(filepath, 'rb')
            pack = fh.read()
            fh.close()
        else:
            pack = filepath
        data = zlib.decompress(pack)
        return pickle.loads(data)

# hierarchy

    def get_chunk(self, slices):
        """
        Returns a **chunk** (sub-array) i.e. the cartesian product of ``slices``
        along the arrays dimensions.
        
        Arguments:
        
          - slices(sequence) a sequence of ``slice`` instances. 
          
        """
        indices, shape = slices_shape2indices_shape(slices, self.shape)
        data = self._chunkdata(indices)
        try:
            return self.new(data, shape=shape)
        except ValueError:
            raise ValueError("invalid for chunk shape for self.child")

    def set_chunk(self, slices, value):
        """
        Modifies in-place the **chunk** (sub-array) i.e. the cartesian product 
        of slices along the array dimensions.
        
        Argumensts:
        
          - slices(sequence) a sequence ``slice`` instances that should be of 
            the same length as the "shape" of the **array**.
          - value(sequence) a sequence of **elements** size of **chunk**
        
        """
        indices, shape = slices_shape2indices_shape(slices, self.shape) #@UnusedVariable
        for index, value in izip(indices, value):
            self[index] = value

    def get_child(self, index):
        """
        Returns the **child** (sub-array) at "index" along the first dimension 
        of ``self``. Equivalent to ``self[index]``.
        
        Arguments:
        
          - index(``int``) index along the first dimension
          
        """
        return self.__getitem__(index)

    def set_child(self, index, value):
        """
        Modifies in-pace the data of the **child** (sub-array) at "index" along
        the  first dimension of ``self``. Equivalent to 
        ``self[indxex] = value``.
        
        Arguments:
        
          - index(``int``) index along the first dimension
          - value(sequence) sequence of **elements** of length of **child**
          
        """
        return self.__setitem__(index, value)

    def iter_child(self, fast=False):
        """
        Returns a ``generator`` over children (sub-arrays) along the first 
        dimension of ``self``. If "fast" is ``True`` all returned ``objects``
        are the same ``NdArray`` instance with monkey-patched data.
        
        Arguments
        
          - fast(``bool``) If ``True`` only one instance of a ``NdArray`` is 
            constructed.
          
        """
        if not fast:
            return self.__iter__()
        else:
            return self._iterfast()

    def get_item(self, i):
        """
        Returns the *i-th* **item** at "index" in the flattened array. The 
        result is a ``tuple`` of **item** size withe elements of ``type``
        corresponding to the array typecode.
        
        Arguments:
        
          - i(``int``) **item** number (index)
          
        """
        return self._itemdata((i,))

    def set_item(self, i, value):
        """
        Modifies in-place the *i-th* **item** in the flattened **array**.
        
        Arguments:
        
          - i(``int``) **item** number (index)
          - value(iterable) sequence of **item** size with **elements** of 
            valid ``type``
        
        """
        step = self.shape[-1]
        start = i * step
        stop = start + step
        try:
            self.data[start:stop] = value
        except TypeError:
            # non-array to array assignment
            for i, j in enumerate(xrange(start, stop)):
                self.data[j] = value[i]

    def iter_item(self):
        """
        Returns a generator over **items** in the flattened array.
        
        """
        itembounds = self._itembounds()
        for start, stop in itembounds:
            yield tuple(self.data[start:stop])

    def map_item(self, function, *args, **kwargs):
        """
        Applies a function in-place on the **items** of the flattened **array**.
        
        Arguments:
        
          - function(callable) will be applied on the **array** **items**.
          
        Additional arguments and keyworded arguments are passed to the supplied
        callable ``object``.

        """
        for i, (start, stop) in enumerate(self._itembounds()):
            value = function(self.data[start:stop], *args, **kwargs)
            self.set_item(i, value)

    def fill_item(self, value):
        """
        Modifies in-place the value of each **item** in the **array**.
        
        Arguments:
        
          - value(sequence) sequence of **item** size with **elements** of 
            valid ``type``
          
        """
        if len(value) != self.shape[-1]:
            raise ValueError("value does not match shape")
        self._checkdata(value, self.typecode, self.store)
        itembounds = self._itembounds()
        for start, stop in itembounds:
            try:
                self.data[start:stop] = value
            except TypeError:
                for i, j in enumerate(xrange(start, stop)):
                    self.data[j] = value[i]

    def max_item(self):
        """
        Returns the largest **item** in the flattened **array**.
        
        """
        return tuple(max(tuple(self.iter_item())))

    def min_item(self):
        """
        Returns the smallest **item** in the flattened **array**.
        
        """
        return tuple(min(tuple(self.iter_item())))


class NuBox(NdArray):
    """
    The ``NuBox`` is an array object and sub-class of ``NdArray``. The main 
    addition is that it holds metadata, supports a hierarchy i.e. an **array** 
    of sub-arrays. The main difference between ``NuBox`` and ``NdArray`` is in 
    the constructor. Further the ``NuBox`` has additional properties 
    (attributes).
    
    Just like ``NdArray``, ``NuBox`` provides methods and syntactic sugar to 
    access all the sub-arrays conveniently:
    
      - **chunk** *via* ``array.get_chunk``, ``array.set_chunk`` or 
        ``array[<slice>]`` returns: ``self``
      - **child** *via* ``array.get_child``, ``array.set_child`` or 
        ``array[<int>]`` returns: ``self.child``
      - **item** *via* ``array.get_item`` or ``array.set_item`` returns 
        ``tuple``
      - **element** *via* ``array[<tuple>]`` returns: ``type``

    An ``NuBox`` has the following additional to ``NdArray`` proprties, which 
    are also constructor arguments only "dummy" is strictly mandatory.:
        
      - "meta" metadata for the current instance
      - "childrenkwargs" a list, which defines the constructor arguments 
        (keyworded arguments) of all children
      - "parent" defines the ``NuBox`` sub-class of the **parent** 
         instance
      - "child" defines the ``NuBox`` sub-class of **child** instances
      - "dummy" defines a prototype **item** of the array, must be compatible
        with the last dimension of "shape"

    This translates to the following constructor elements:
    
      - "meta" (``dict``) any dictionary which can be serialized
      - "childrenkwargs" (``list``) a list of ``dict`` one for each **child**
        this is what is passed to the "child" constructor.
      - "parent" (``NuBox`` or None) A ``NuBox`` sub-class (not instance)
      - "child" (``NuBox`` or NOCHILD) A ``NuBox`` sub-class (not instance)
      - "dummy" (``tuple``) a ``tuple`` of length equal to the last dimension of
        array
        
    Additional properties (attributes) and arguments are the same as for 
    ``NdArray``.

    """
    clsdata = None # defaults to constructor
    clsmeta = () # instance meta is a dict
    clschildrenkwargs = () # instance chilidrenkwargs is a list
    clsparent = DEFAULT # defaults to Nubox
    clschild = DEFAULT  # defaults to NuBox
    clsdummy = None # no default
    clsshape = None # no default, determined by dummy and data
    clstypecode = None # no default, determined by dummy
    clsstore = DEFAULT # defaults to 'array'
    # this is redundant with data and shape or dummy and not an constructor 
    # argument
    clsrank = ANY       # defaults to any rank

    def __init__(self, data=None, meta=None, childrenkwargs=None, parent=None,
                       child=None, dummy=None, shape=None, typecode=None,
                       store=None):


        self._setmeta(meta or {}) # copy else new dict
        self._setparent(parent)
        self._setchild(child)
        self._setdummy(dummy)
        childrenkwargs, data, shape, typecode, store, copy = \
            self._fromdata(childrenkwargs, (data or ()), shape, typecode, store)
        self._setchildrenkwargs(childrenkwargs)

        super(NuBox, self).__init__(data, shape, typecode, store, copy)

    def _getmeta(self):
        # for future compability
        return self.__meta

    def _setmeta(self, meta):
        # this is effectively a shallow copy
        classmeta = dict(self.clsmeta)  # new class metadata
        classmeta.update(meta)          # argument meta overrides clsmeta
        self.__meta = classmeta

    def _getparent(self):
        # for future compability        
        return self.__parent

    def _setparent(self, parent):
        # defaults to NuBox
        # argument parrent overrides clsparent
        if parent:
            if not issubclass(parent, NuBox):
                raise ValueError("parent must be a NuBox subclass")
        elif self.clsparent == DEFAULT:
            parent = NuBox
        else:
            parent = self.clsparent
        self.__parent = parent

    def _getchild(self):
        # for future compability
        return self.__child

    def _setchild(self, child):
        if child is not None:
            pass
        elif self.clschild == DEFAULT:
            child = NuBox
        else:
            child = self.clschild
        if (child != NOCHILD) and not issubclass(child, NuBox):
            raise ValueError("child must be a NuBox subclass")
        self.__child = child

    def _getdummy(self):
        return self.__dummy

    def _setdummy(self, dummy):
        if dummy is not None:
            pass
        elif self.clsdummy is not None:
            dummy = self.clsdummy
        else:
            raise ValueError("dummy not given")
        # type checks
        if not isinstance(dummy, tuple):
            raise TypeError("dummy must be a tuple")
        if len(dummy) == 0:
            raise ValueError("dummy must not be empty")
        if self.clstypecode:
            dummy_typecode = gettype(dummy[0])[1] # e.g. 'l'
            if not compatypecodes((dummy_typecode, self.clstypecode)):
                raise ValueError("dummy type must match typecode")
        self.__dummy = dummy

    def _getchildrenkwargs(self):
        return self.__childrenkwargs

    def _setchildrenkwargs(self, childrenkwargs):
        if childrenkwargs and not self.child:
            raise ValueError("data must match dummy size")
        if childrenkwargs is not None:
            childrenkwargs = list(childrenkwargs)
        else:
            childrenkwargs = list(self.clschildrenkwargs)
        if not isinstance(childrenkwargs, list):
            raise TypeError("children kwargs must be a list")
        self.__childrenkwargs = childrenkwargs

    meta = property(_getmeta, _setmeta)
    parent = property(_getparent, _setparent)
    child = property(_getchild, _setchild)
    dummy = property(_getdummy, _setdummy)
    childrenkwargs = property(_getchildrenkwargs, _setchildrenkwargs)

# persistance

    def _kwargs(self):
        kwargs = super(NuBox, self)._kwargs()
        kwargs.update({'meta':self.meta,
                       'parent':self.parent,
                       'child':self.child,
                       'dummy':self.dummy,
                       'childrenkwargs':self.childrenkwargs
                       })
        return kwargs

    def _childkwargs(self, index):
        itemkwargs = self._kwargs()
        itemkwargs['parent'] = self.__class__ # defines self as parent
        itemkwargs['shape'] = itemkwargs['shape'][1:] # lower rank
        itemkwargs.pop('child')
        itemkwargs.pop('childrenkwargs')
        itemkwargs.update(self.childrenkwargs[index]) # these take precedence
        return itemkwargs

    def __copy__(self):
        # this is a shallow copy
        kwargs = self._kwargs()
        # copy=True by default
        return self.__class__(self.data, **kwargs)

    def new(self, data=None, **kwargs):
        """
        Creates a new instance of ``NuBox`` with a signature identical to the 
        default constructor with the key difference that it by default clones
        all properties of the current instance. If no arguments are given this
        is identical to ``deepcopy(self)``.
        
        Arguments:
        
          - data see: ``NuBox.__init__``
        
        Keyworded arguments are the same as for the ``NuBox`` constructor.
          
        """
        new_kwargs = self._kwargs()
        new_kwargs.update(kwargs)
        new = self.__class__((data or self.data), **new_kwargs)
        new.childrenkwargs = deepcopy(new.childrenkwargs)
        new.meta = deepcopy(new.meta)
        new.__data = deepcopy(new.data)
        return new

# constructors (cannot use most private methods)

    def _fromdata(self, children, data, shape, typecode, store):
        if typecode is not None:
            pass
        elif self.clstypecode is not None:
            typecode = self.clstypecode
        else:
            typecode = gettype(self.dummy[0])[1]
        if store is not None:
            pass
        elif self.clsstore != DEFAULT:
            store = self.clsstore
        else:
            store = 'array'
        if shape is not None:
            pass
        else:
            shape = self.clsshape

        #shape = shape or self.clsshape
        if data:
            try:
                item = data[0]
            except TypeError:
                raise TypeError("data must be iterable")
            isatomic, isiterable, isarray, isarrayitem, islistitem = checkitem(item)
        else:
            isatomic, isiterable, isarray, isarrayitem, islistitem = \
            False, False, False, False, False

        if (shape is not None) and (isarrayitem or islistitem or not data):
            children, data, shape, copy = self._fromshape(children, data, \
                                                        shape, typecode, store)
        elif isatomic and (isarrayitem or islistitem):
            children, data, shape, copy = self._fromitems(children, \
                                                   data, shape, typecode, store)
        elif isarray:
            children, data, shape, copy = self._fromarrays(children, \
                                                   data, shape, typecode, store)
        elif isiterable:
            children, data, shape, copy = self._fromiterables(children, \
                                         data, shape, typecode, store)
        else:
            raise ValueError("shape or data must be given")
        return children, data, shape, typecode, store, copy

    def _fromshape(self, children, data, shape, typecode, store):
        # no children along last dimension
        if self.clsrank != ANY and len(shape) != self.clsrank:
            raise ValueError("shape must match rank")
        real_shape = shape[:-1]
        if not data:
            # create fake flat data
            if real_shape:
                data = self.dummy * reduce(operator.mul, real_shape)
            elif shape:
                data = self.dummy
            else:
                data = ()
        if (self.child != NOCHILD) and not children:
            # create fake nested children
            basechild = {'childrenkwargs':[]}
            for dim in real_shape[::-1]: # backwards over real dimensions
                outerchild = copy(basechild)
                outerchild['childrenkwargs'] = []
                for i in xrange(dim): #@UnusedVariable
                    innerchild = deepcopy(basechild)
                    outerchild['childrenkwargs'].append(innerchild)
                basechild = outerchild
            children = basechild['childrenkwargs']
        # None -> []
        return children or [], data, shape, True

    def _fromitems(self, children, data, shape, typecode, store):
        if children:
            raise ValueError("children needs shape")
        el_len = len(self.dummy)
        el_num = len(data) / el_len
        el_num_int = int(floor(el_num))
        if el_num_int != el_num:
            raise ValueError("sequence length must be multiple of dummy")
        if el_num_int == 1:
            shape = (el_len,)
        else:
            shape = (el_num_int, el_len)
        while len(shape) < (self.clsrank or 0):
            shape = (1,) + shape
        return self._fromshape(children, data, shape, typecode, store)

    def _fromarrays(self, children, data, shape, typecode, store):
        if children:
            raise ValueError("got children and arrays")
        item = data[0]
        # this creates a new empty array of the valid typecode or an empty list
        all_data = item.data[0:0]
        # if types of store changes tell NdArray to copy
        copy = True if (isinstance(all_data, array) and (store != 'array')) or \
                       (isinstance(all_data, list) and (store != 'list')) else \
               False
        children = []
        for arr in data:
            if arr.shape != item.shape:
                raise ValueError("NuBoxs must have same shape")
            # copy meta like data
            children.append(deepcopy(arr._kwargs()))
            all_data.extend(arr.data)
        shape = (len(data),) + item.shape
        return children, all_data, shape, copy

    def _fromiterables(self, children, data, shape, typecode, store):
        if shape:
            raise ValueError("got nested data and shape")
        child_kwargs = {'typecode':typecode,
                        'store':store,
                        'dummy':self.dummy,
                        'parent':self.__class__}
        if not children:
            nuboxs = [self.child(item, **child_kwargs) for item in data]
        else:
            if len(children) != len(data):
                raise ValueError("lengths of data and children must match")
            nuboxs = []
            for item, child in izip(data, children):
                child.update(child_kwargs)
                nuboxs.append(self.child(item, **child))
        return self._fromarrays(None, nuboxs, shape, typecode, store)

# hierarchy

    def __getitem__(self, index):
        if isinstance(index, tuple):
            # returns element same as NdArray
            return super(NuBox, self).__getitem__(index)
        elif isinstance(index, slice):
            # returns smaller self
            indices = xrange(*index.indices(len(self))) # index is of slice type
            ch_data = self._childrendata(indices)
            ch_kwargs = [self._childkwargs(index) for index in indices]
            shape = (len(indices),) + self.shape[1:]
            return self.new(ch_data, shape=shape, childrenkwargs=ch_kwargs)
        else:
            # returns a child
            childkwargs = self._childkwargs(index)
            data = self._childrendata((index,))
            return self.child(data, **childkwargs)

    def __iter__(self):
        # is a generator
        # returns real self.child instances
        if self.childrenkwargs:
            childbounds = self._childrenbounds()
            for i, (start, stop) in enumerate(childbounds):
                data = self.data[start:stop]
                childkwargs = self._childkwargs(i)
                yield self.child(data, **childkwargs)

    def get_chunk(self, slices):
        """
        Returns a **chunk** (sub-array) i.e. the cartesian product of ``slices``
        along the arrays dimensions.
        
        Arguments:
        
          - slices(iterable) an iterable of ``slice`` instances. 
          
        """
        indices, shape = slices_shape2indices_shape(slices, self.shape)
        data = self._chunkdata(indices)
        try:
            return self.new(data, shape=shape, child=NOCHILD, childrenkwargs=[])
        except ValueError:
            raise ValueError("invalid for chunk shape for self.child")

# the following provides the default hierarchy

class NuBoxScalar(NuBox):
    """
    A ``NuBoxScalar`` has rank 1 and has no **children**.
    
    see: ``NuBox``
    """
    clschild = NOCHILD
    clsrank = 1


class NuBoxVector(NuBoxScalar):
    """
    A ``NuBoxVector`` has rank 2 and hold ``NuBoxScalars``.
    
    see: ``NuBox``
    """
    clschild = NuBoxScalar
    clsrank = 2


class NuBoxArray(NuBoxVector):
    """
    A ``NuBoxArray`` has rank 3 and hold ``NuBoxVectors``.
    
    see: ``NuBox``    
    """
    clschild = NuBoxVector
    clsrank = 3


class NuBoxCube(NuBoxArray):
    """
    A ``NuBoxCube`` has rank 4 and hold ``NuBoxArrays``.
    
    see: ``NuBox``
    """
    clschild = NuBoxArray
    clsrank = 4

NuBoxScalar.clsparent = NuBoxVector
NuBoxVector.clsparent = NuBoxArray
NuBoxArray.clsparent = NuBoxCube
